ui_utils.js ➔ watchScroll   B
last analyzed

Complexity

Conditions 5

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 19
dl 0
loc 29
rs 8.9833
c 0
b 0
f 0
1
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2
/* Copyright 2012 Mozilla Foundation
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
17
'use strict';
18
19
var CSS_UNITS = 96.0 / 72.0;
20
var DEFAULT_SCALE = 'auto';
21
var UNKNOWN_SCALE = 0;
22
var MAX_AUTO_SCALE = 1.25;
23
var SCROLLBAR_PADDING = 40;
24
var VERTICAL_PADDING = 5;
25
26
// optimised CSS custom property getter/setter
27
var CustomStyle = (function CustomStyleClosure() {
28
29
  // As noted on: http://www.zachstronaut.com/posts/2009/02/17/
30
  //              animate-css-transforms-firefox-webkit.html
31
  // in some versions of IE9 it is critical that ms appear in this list
32
  // before Moz
33
  var prefixes = ['ms', 'Moz', 'Webkit', 'O'];
34
  var _cache = {};
35
36
  function CustomStyle() {}
37
38
  CustomStyle.getProp = function get(propName, element) {
39
    // check cache only when no element is given
40
    if (arguments.length === 1 && typeof _cache[propName] === 'string') {
41
      return _cache[propName];
42
    }
43
44
    element = element || document.documentElement;
45
    var style = element.style, prefixed, uPropName;
46
47
    // test standard property first
48
    if (typeof style[propName] === 'string') {
49
      return (_cache[propName] = propName);
50
    }
51
52
    // capitalize
53
    uPropName = propName.charAt(0).toUpperCase() + propName.slice(1);
54
55
    // test vendor specific properties
56
    for (var i = 0, l = prefixes.length; i < l; i++) {
57
      prefixed = prefixes[i] + uPropName;
58
      if (typeof style[prefixed] === 'string') {
59
        return (_cache[propName] = prefixed);
60
      }
61
    }
62
63
    //if all fails then set to undefined
64
    return (_cache[propName] = 'undefined');
65
  };
66
67
  CustomStyle.setProp = function set(propName, element, str) {
68
    var prop = this.getProp(propName);
69
    if (prop !== 'undefined') {
70
      element.style[prop] = str;
71
    }
72
  };
73
74
  return CustomStyle;
75
})();
76
77
function getFileName(url) {
78
  var anchor = url.indexOf('#');
79
  var query = url.indexOf('?');
80
  var end = Math.min(
81
    anchor > 0 ? anchor : url.length,
82
    query > 0 ? query : url.length);
83
  return url.substring(url.lastIndexOf('/', end) + 1, end);
84
}
85
86
/**
87
 * Returns scale factor for the canvas. It makes sense for the HiDPI displays.
88
 * @return {Object} The object with horizontal (sx) and vertical (sy)
89
                    scales. The scaled property is set to false if scaling is
90
                    not required, true otherwise.
91
 */
92
function getOutputScale(ctx) {
93
  var devicePixelRatio = window.devicePixelRatio || 1;
94
  var backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
95
                          ctx.mozBackingStorePixelRatio ||
96
                          ctx.msBackingStorePixelRatio ||
97
                          ctx.oBackingStorePixelRatio ||
98
                          ctx.backingStorePixelRatio || 1;
99
  var pixelRatio = devicePixelRatio / backingStoreRatio;
100
  return {
101
    sx: pixelRatio,
102
    sy: pixelRatio,
103
    scaled: pixelRatio !== 1
104
  };
105
}
106
107
/**
108
 * Scrolls specified element into view of its parent.
109
 * element {Object} The element to be visible.
110
 * spot {Object} An object with optional top and left properties,
111
 *               specifying the offset from the top left edge.
112
 */
113
function scrollIntoView(element, spot) {
114
  // Assuming offsetParent is available (it's not available when viewer is in
115
  // hidden iframe or object). We have to scroll: if the offsetParent is not set
116
  // producing the error. See also animationStartedClosure.
117
  var parent = element.offsetParent;
118
  var offsetY = element.offsetTop + element.clientTop;
119
  var offsetX = element.offsetLeft + element.clientLeft;
120
  if (!parent) {
121
    console.error('offsetParent is not set -- cannot scroll');
122
    return;
123
  }
124
  while (parent.clientHeight === parent.scrollHeight) {
125
    if (parent.dataset._scaleY) {
126
      offsetY /= parent.dataset._scaleY;
127
      offsetX /= parent.dataset._scaleX;
128
    }
129
    offsetY += parent.offsetTop;
130
    offsetX += parent.offsetLeft;
131
    parent = parent.offsetParent;
132
    if (!parent) {
133
      return; // no need to scroll
134
    }
135
  }
136
  if (spot) {
137
    if (spot.top !== undefined) {
138
      offsetY += spot.top;
139
    }
140
    if (spot.left !== undefined) {
141
      offsetX += spot.left;
142
      parent.scrollLeft = offsetX;
143
    }
144
  }
145
  parent.scrollTop = offsetY;
146
}
147
148
/**
149
 * Helper function to start monitoring the scroll event and converting them into
150
 * PDF.js friendly one: with scroll debounce and scroll direction.
151
 */
152
function watchScroll(viewAreaElement, callback) {
153
  var debounceScroll = function debounceScroll(evt) {
154
    if (rAF) {
155
      return;
156
    }
157
    // schedule an invocation of scroll for next animation frame.
158
    rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
159
      rAF = null;
160
161
      var currentY = viewAreaElement.scrollTop;
162
      var lastY = state.lastY;
163
      if (currentY !== lastY) {
164
        state.down = currentY > lastY;
165
      }
166
      state.lastY = currentY;
167
      callback(state);
168
    });
169
  };
170
171
  var state = {
172
    down: true,
173
    lastY: viewAreaElement.scrollTop,
174
    _eventHandler: debounceScroll
175
  };
176
177
  var rAF = null;
178
  viewAreaElement.addEventListener('scroll', debounceScroll, true);
179
  return state;
180
}
181
182
/**
183
 * Use binary search to find the index of the first item in a given array which
184
 * passes a given condition. The items are expected to be sorted in the sense
185
 * that if the condition is true for one item in the array, then it is also true
186
 * for all following items.
187
 *
188
 * @returns {Number} Index of the first array element to pass the test,
189
 *                   or |items.length| if no such element exists.
190
 */
191
function binarySearchFirstItem(items, condition) {
192
  var minIndex = 0;
193
  var maxIndex = items.length - 1;
194
195
  if (items.length === 0 || !condition(items[maxIndex])) {
196
    return items.length;
197
  }
198
  if (condition(items[minIndex])) {
199
    return minIndex;
200
  }
201
202
  while (minIndex < maxIndex) {
203
    var currentIndex = (minIndex + maxIndex) >> 1;
204
    var currentItem = items[currentIndex];
205
    if (condition(currentItem)) {
206
      maxIndex = currentIndex;
207
    } else {
208
      minIndex = currentIndex + 1;
209
    }
210
  }
211
  return minIndex; /* === maxIndex */
212
}
213
214
/**
215
 * Generic helper to find out what elements are visible within a scroll pane.
216
 */
217
function getVisibleElements(scrollEl, views, sortByVisibility) {
218
  var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight;
219
  var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth;
220
221
  function isElementBottomBelowViewTop(view) {
222
    var element = view.div;
223
    var elementBottom =
224
      element.offsetTop + element.clientTop + element.clientHeight;
225
    return elementBottom > top;
226
  }
227
228
  var visible = [], view, element;
229
  var currentHeight, viewHeight, hiddenHeight, percentHeight;
230
  var currentWidth, viewWidth;
231
  var firstVisibleElementInd = (views.length === 0) ? 0 :
232
    binarySearchFirstItem(views, isElementBottomBelowViewTop);
233
234
  for (var i = firstVisibleElementInd, ii = views.length; i < ii; i++) {
235
    view = views[i];
236
    element = view.div;
237
    currentHeight = element.offsetTop + element.clientTop;
238
    viewHeight = element.clientHeight;
239
240
    if (currentHeight > bottom) {
241
      break;
242
    }
243
244
    currentWidth = element.offsetLeft + element.clientLeft;
245
    viewWidth = element.clientWidth;
246
    if (currentWidth + viewWidth < left || currentWidth > right) {
247
      continue;
248
    }
249
    hiddenHeight = Math.max(0, top - currentHeight) +
250
      Math.max(0, currentHeight + viewHeight - bottom);
251
    percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0;
252
253
    visible.push({
254
      id: view.id,
255
      x: currentWidth,
256
      y: currentHeight,
257
      view: view,
258
      percent: percentHeight
259
    });
260
  }
261
262
  var first = visible[0];
263
  var last = visible[visible.length - 1];
264
265
  if (sortByVisibility) {
266
    visible.sort(function(a, b) {
267
      var pc = a.percent - b.percent;
268
      if (Math.abs(pc) > 0.001) {
269
        return -pc;
270
      }
271
      return a.id - b.id; // ensure stability
272
    });
273
  }
274
  return {first: first, last: last, views: visible};
275
}
276
277
/**
278
 * Event handler to suppress context menu.
279
 */
280
function noContextMenuHandler(e) {
281
  e.preventDefault();
282
}
283
284
/**
285
 * Returns the filename or guessed filename from the url (see issue 3455).
286
 * url {String} The original PDF location.
287
 * @return {String} Guessed PDF file name.
288
 */
289
function getPDFFileNameFromURL(url) {
290
  var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/;
291
  //            SCHEME      HOST         1.PATH  2.QUERY   3.REF
292
  // Pattern to get last matching NAME.pdf
293
  var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
294
  var splitURI = reURI.exec(url);
295
  var suggestedFilename = reFilename.exec(splitURI[1]) ||
296
                           reFilename.exec(splitURI[2]) ||
297
                           reFilename.exec(splitURI[3]);
298
  if (suggestedFilename) {
299
    suggestedFilename = suggestedFilename[0];
300
    if (suggestedFilename.indexOf('%') !== -1) {
301
      // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf
302
      try {
303
        suggestedFilename =
304
          reFilename.exec(decodeURIComponent(suggestedFilename))[0];
305
      } catch(e) { // Possible (extremely rare) errors:
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
306
        // URIError "Malformed URI", e.g. for "%AA.pdf"
307
        // TypeError "null has no properties", e.g. for "%2F.pdf"
308
      }
309
    }
310
  }
311
  return suggestedFilename || 'document.pdf';
312
}
313
314
var ProgressBar = (function ProgressBarClosure() {
315
316
  function clamp(v, min, max) {
317
    return Math.min(Math.max(v, min), max);
318
  }
319
320
  function ProgressBar(id, opts) {
321
    this.visible = true;
322
323
    // Fetch the sub-elements for later.
324
    this.div = document.querySelector(id + ' .progress');
325
326
    // Get the loading bar element, so it can be resized to fit the viewer.
327
    this.bar = this.div.parentNode;
328
329
    // Get options, with sensible defaults.
330
    this.height = opts.height || 100;
331
    this.width = opts.width || 100;
332
    this.units = opts.units || '%';
333
334
    // Initialize heights.
335
    this.div.style.height = this.height + this.units;
336
    this.percent = 0;
337
  }
338
339
  ProgressBar.prototype = {
340
341
    updateBar: function ProgressBar_updateBar() {
342
      if (this._indeterminate) {
343
        this.div.classList.add('indeterminate');
344
        this.div.style.width = this.width + this.units;
345
        return;
346
      }
347
348
      this.div.classList.remove('indeterminate');
349
      var progressSize = this.width * this._percent / 100;
350
      this.div.style.width = progressSize + this.units;
351
    },
352
353
    get percent() {
354
      return this._percent;
355
    },
356
357
    set percent(val) {
358
      this._indeterminate = isNaN(val);
359
      this._percent = clamp(val, 0, 100);
360
      this.updateBar();
361
    },
362
363
    setWidth: function ProgressBar_setWidth(viewer) {
364
      if (viewer) {
365
        var container = viewer.parentNode;
366
        var scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
367
        if (scrollbarWidth > 0) {
368
          this.bar.setAttribute('style', 'width: calc(100% - ' +
369
                                         scrollbarWidth + 'px);');
370
        }
371
      }
372
    },
373
374
    hide: function ProgressBar_hide() {
375
      if (!this.visible) {
376
        return;
377
      }
378
      this.visible = false;
379
      this.bar.classList.add('hidden');
380
      document.body.classList.remove('loadingInProgress');
381
    },
382
383
    show: function ProgressBar_show() {
384
      if (this.visible) {
385
        return;
386
      }
387
      this.visible = true;
388
      document.body.classList.add('loadingInProgress');
389
      this.bar.classList.remove('hidden');
390
    }
391
  };
392
393
  return ProgressBar;
394
})();
395